[FIX] Rectify: Vacuous Gate Reachability Blind Spot Bypasses Dispatch Admission Control#4162
Merged
Trecek merged 2 commits intoJun 30, 2026
Conversation
_is_vacuous_gate previously checked only property-level guards (pruned steps, live capability uses) and ignored whether the gate step itself was reachable in the post-prune flow graph. Route-repair in _prune_skipped_steps can redirect upstream steps directly to the gate when guarded steps are pruned, making the gate reachable and executable. Add _gate_reachable_post_prune which uses the canonical _analysis_graph._build_step_graph (handles on_exhausted, on_result.routes, and skip_when_false bypass edges) plus bfs_reachable from the entry step. _is_vacuous_gate now returns False when the gate is reachable, forcing admission control to report dispatch_feasible=False for Codex+implementation pipelines. Thread post_prune_recipe as a required kwarg (no default) so any future caller omission is a TypeError rather than a silent opt-out. load_recipe enforcement upgraded from soft annotation to hard block: when dispatch_feasible=False, return a failure envelope without recipe content, matching the open_kitchen enforcement pattern. Update tests that pinned the buggy vacuous-gate behavior as correct: test_codex_implementation_dispatch_infeasible, test_codex_open_kitchen_* now assert dispatch_feasible=False with gate in infeasible_steps. Add new test_recipe_composition_vacuous_gate.py with unit tests for _is_vacuous_gate reachable/unreachable paths and real recipe integration tests for all three gate-equipped recipes (implementation, remediation, implementation-groups). Add AST invariant test verifying _compute_capability_feasibility forwards post_prune_recipe to _is_vacuous_gate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_gate_reachable_post_prune used BFS from the entry step, which failed when unrelated pruning (e.g. claim_and_resolve with default-condition redirect to register_clone_failure) disconnected the entry from the main pipeline. The gate was routable via create_impl_worktree but unreachable from clone, causing false "vacuous" classification. Replace with incoming-edge check via _collect_all_route_targets: a gate with any incoming routing edge from a surviving step is considered reachable. This is conservative (may over-reject if the routing step is itself unreachable) but safe for admission control where false negatives (allowing DOA pipelines) are dangerous. Also fix _StubStep missing sub_recipe field and open_kitchen test asserting dispatch_feasible in an envelope that doesn't include it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Jun 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
_is_vacuous_gate(_recipe_composition.py:178-213) declares a capability gate step vacuous by checking whether guarded steps were pruned and whether surviving steps consume the capability ingredient — but never checks whether the gate step itself is reachable in the post-prune flow graph. Route-repair in_prune_skipped_stepscan redirect upstream steps to point directly to the gate, making it reachable and executable even after all guarded steps are pruned. The result: Codex+implementation pipelines pass admission control, execute 21 minutes of side-effecting steps, then fail deterministically at the gate.The architectural weakness is that admission-control functions (
_compute_capability_feasibility,_is_vacuous_gate) operate on flat step dictionaries using property-only checks, while the recipe layer has mature BFS reachability machinery (bfs_reachable,_build_step_graphin_analysis_bfs.py) that is only consumed by semantic rules viaValidationContext— never by admission control.The immunity approach: make graph-awareness a structural requirement for all admission-control decisions by threading the post-prune step graph through
_compute_capability_feasibilityand_is_vacuous_gate, and adding a reachability gate that short-circuits the vacuous determination when the gate step is unreachable.Requirements
Investigation: Codex Pipeline DOA Bypass via _is_vacuous_gate Reachability Blind Spot
Date: 2026-06-30
Scope: Why Codex+implementation pipeline passes dispatch_infeasible admission control and runs 21 minutes of irreversible side effects before dying at gate_backend_write, despite PR #4094 and #4130 adding capability admission control.
Mode: Deep Analysis (2 exploration batches + adversarial challenge + post-report validation; 9 subagents)
Summary
The Codex+implementation pipeline bypasses admission control because
_is_vacuous_gate(_recipe_composition.py:178-213, introduced by PR #4130 on June 27) incorrectly declaresgate_backend_writeas "vacuous" — concluding that since all guarded steps were pruned, the gate has nothing left to guard and should not triggerdispatch_infeasible. But_is_vacuous_gateonly checks whether surviving steps use the capability ingredient in theirwith_args; it does not check whether the gate step itself remains reachable in the post-prune flow graph. After_prune_skipped_stepsprunesimplement, route-repair redirectscreate_impl_worktree.on_successfromimplementtogate_backend_write, making the gate directly reachable. The pipeline is dispatched, runs 11 side-effecting steps (~21 minutes), thengate_backend_writefires and routes torelease_issue_failure— exactly the DOA outcome that admission control was designed to prevent.This is the third oscillation of a pendulum pattern: PR #4094 blocks DOA pipelines → PR #4130 unblocks (vacuous-gate exemption goes too far) → today's failure. The structural cause is that admission control operates on heuristic inference (pattern-matching callables against registries) rather than author-declared intent (the
purpose_criticalstep annotation proposed in PR #4094's REQ-SCHEMA-001, which was never implemented).Root Cause
Primary (architectural):
_is_vacuous_gateat_recipe_composition.py:178-213has a reachability blind spot. It determines vacuity by checking two conditions: (1) at least one pre-prune step with a matchingskip_when_falseguard was pruned (pruned_for_capability), and (2) no surviving step (other than the gate itself) references the capability ingredient inwith_args(live_uses_capability). Both conditions are met for Codex+implementation. But the function never checks whether the gate step is reachable from any surviving step via routing fields (on_success,on_failure,on_result, etc.). After route-repair,create_impl_worktree.on_successpoints directly togate_backend_write. The gate is reachable, will execute, and will deterministically fail — but_is_vacuous_gatereturnsTrue. [SUPPORTED]Proximate (why the reachability check is missing): PR #4130 was solving a real false-positive problem — #4094's admission control blocked all Codex+implementation runs even though the pruned recipe's gate was intended to be a defense-in-depth terminal. The vacuous-gate concept is architecturally correct (a gate CAN be genuinely unreachable), but the implementation checks the wrong predicate. It asks "does any surviving step consume the capability?" when it should also ask "is the gate step itself reachable from the entry point?" [SUPPORTED]
Why prior fixes did not address this: The fix chain follows a pendulum pattern:
The
purpose_criticalper-step annotation proposed in PR #4094's design (REQ-SCHEMA-001) was never implemented. The system uses inference-based heuristics layered on inference-based heuristics, each layer correcting the prior while introducing new failure surface. [SUPPORTED]Affected Components
src/autoskillit/recipe/_recipe_composition.py:178-213—_is_vacuous_gate(): missing reachability check [SUPPORTED]src/autoskillit/recipe/_recipe_composition.py:109-175—_compute_capability_feasibility(): calls_is_vacuous_gateand trusts its result [SUPPORTED]src/autoskillit/recipe/_recipe_composition.py:401-524—_prune_skipped_steps()(function); route-repair logic at lines 460-520 redirectscreate_impl_worktree.on_successtogate_backend_write[SUPPORTED]src/autoskillit/recipe/_api.py:405-412—load_and_validate(): calls_compute_capability_feasibility, propagatesdispatch_feasible=True[SUPPORTED]src/autoskillit/server/tools/tools_kitchen.py:844-847—open_kitchenfeasibility check: passes becausedispatch_feasible=True[SUPPORTED]src/autoskillit/recipes/implementation.yaml:298-365—create_impl_worktree(no guard) →implement(guarded, line 328) →gate_backend_write(no guard) topology [SUPPORTED]src/autoskillit/recipes/remediation.yaml:335-405— Same topology (create_impl_worktreeat 335,implementat 347,gate_backend_writeat 396-405) [SUPPORTED]src/autoskillit/recipes/implementation-groups.yaml:261-323— Same topology [SUPPORTED]src/autoskillit/recipe/_analysis_bfs.py:17,58—bfs_reachable()and_build_step_graph()— existing reachability machinery not used by_is_vacuous_gate[SUPPORTED]src/autoskillit/core/types/_type_constants.py:95-118—BACKEND_CAPABILITY_INGREDIENTS,CAPABILITY_GATE_CALLABLES,CAPABILITY_INGREDIENT_TO_SKIP_GUARDregistries [SUPPORTED]Data Flow
AUTOSKILLIT_FEATURES__CODEX_BACKEND=true AUTOSKILLIT_AGENT_BACKEND__BACKEND=codex autoskillit orderget_backend("codex")→CodexBackend→capabilities.git_metadata_writable=False(codex.py:594)open_kitchen(name="implementation")→_backend_capability_overrides()→{"backend_supports_git_write": "false"}(_auto_overrides.py:27-28)_session_overrides.update(_backend_capability_overrides(...))injects"false"(line 659);_promote_capability_keys(_config_layer, _session_overrides)copies it into config-authoritative layer so it wins the merge (line 661); merged at line 662load_and_validate("implementation", ingredient_overrides={"backend_supports_git_write": "false"})→_prune_skipped_steps():implement(line 328:skip_when_false: inputs.backend_supports_git_write)create_impl_worktree.on_successredirected fromimplement→gate_backend_write(lines 464-465, 484-485)retry_worktree,fix,merge_gate_fix,rebase_conflict_fix,resolve_review,resolve_pre_review_conflicts— same guard_compute_capability_feasibility()evaluates survivinggate_backend_write:tool=run_python✓, callable inCAPABILITY_GATE_CALLABLES✓,all_falsy=True✓,on_exhausted="escalate"(schema default) ✓_is_vacuous_gate():pruned_for_capability=True(implement pruned),live_uses_capability=False(no surviving step hasbackend_supports_git_writein with_args) → returnsTrueinfeasible→dispatch_feasible=Trueopen_kitchenreturnssuccess=True→ kitchen gate opens → orchestrator starts executingbootstrap_clone(~3s),claim_and_resolve_issue(~3s),create_and_publish_branch(~3s),make-plan(~13min),review-approach(~5min),dry-walkthrough(~8min),create_impl_worktree(~1s)gate_backend_write(backend_supports_git_write="false")→{"backend_capable": "false"}→ routes torelease_issue_failurefail, clone registered as error, worktree abandoned. ~21 minutes wasted.Test Gap Analysis
Tests pin the bug as correct behavior — the same pattern as the June 12 investigation:
tests/server/test_backend_ingredient_injection.py:554-564— asserts Codex + implementationopen_kitchenreturnssuccess=Trueanddispatch_feasible=True(changed by PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130 fromsuccess=False, kitchen=dispatch_infeasible) [SUPPORTED]tests/recipe/test_bundled_recipes_dispatch_ready.py:268-282— function namedtest_codex_implementation_dispatch_infeasiblebut assertsdispatch_feasible is True(name-assertion mismatch from pendulum) [SUPPORTED]tests/recipe/test_bundled_recipes_dispatch_ready.py:329-350—test_codex_capability_gate_recipesuses disjunctive assertion accepting EITHERTrueORFalse— cannot catch a new class of regression [SUPPORTED]Similar Patterns
_check_dispatch_feasibility(_preflight.py:34-114) solves the analogous problem forrun_skillsteps — iterates steps, checks per-step provider profile, skips steps routed to a capable backend. Same pattern needed forrun_pythoncapability gates. [SUPPORTED]failure-verdict-bypass-reachable(Rectify: Failure-Verdict Bypass Immunity via Static Reachability Rule #3630) already performs terminal-classified reachability usingbfs_reachable()and_build_step_graph(). The machinery for the correct check exists. [SUPPORTED]create_impl_worktreestep executes at the orchestrator/host level (unsandboxedrun_cmd), not inside a Codex session. All host-side git operations succeed on Codex — the irreducible gap is onlyimplementcommitting inside the sandbox. [SUPPORTED]Design Intent Findings
_is_vacuous_gate(PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130, commit1112c0f3f, 2026-06-27): Introduced to fix false-positive blocking from PR [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094. Commit message: "A gate step (gate_backend_write) that guards already-pruned operations survives pruning and blocks the entire recipe. ... Three independent subsystems — step pruning, capability feasibility, and per-step backend override — operate without cross-communication." The function's purpose is to detect genuinely unreachable gates and exempt them from infeasibility. The purpose is correct; the reachability predicate is incomplete. [SUPPORTED]_compute_capability_feasibility(PR [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094, commitcfb6ecc51, 2026-06-12): Introduced as the "definitive" admission control after 84+ downstream fixes. PR body: "84[FIX]commits over ~3 weeks have patched individual links in the capability injection chain without closing the architectural gap." REQ-SCHEMA-001 proposed apurpose_criticalstep annotation; it was never implemented — the heuristic approach was substituted. [SUPPORTED]gate_backend_writestep (PR Implementation Plan: Backend-Conditional Routing — gate_backend_write Step #3960, commit207deda91, 2026-06-09): Converts DOA pipeline ending from false-success to honest-failure. Never intended to prevent wasted work — only to make the waste end honestly. [SUPPORTED]BackendCapabilities.git_metadata_writable(PR [fix] Codex Pipeline Falseno_changesClassification — Part A (Cascade-Critical Bugs 1–3) #3844, commit5a971233d, 2026-06-06): Documents the Codex sandbox constraint. Static declaration, no runtime probe. Correct. [SUPPORTED]Historical Context
This is the third oscillation of a confirmed pendulum pattern:
The pendulum:
Fix chain (all verified by commit message + diff):
#3844 → #3880 → #3892 → #3902 → #3960 → #3973 → #3980 → #3981 → #3995 → #4068 → #4071 → #4075 → #4086 → #4087 → #4094 → #4130
Prior investigations directly on this topic:
investigation_codex_gate_backend_write_recurrence_2026-06-12_083200.md(June 12, deep analysis, 9 subagents),investigation_codex_dispatch_infeasible_2026-06-12_151200.md(June 12, deep analysis). Both identified the root cause correctly but proposed fixes that were either not implemented as designed (purpose_critical) or introduced new gaps (vacuous-gate).This is a recurring pattern — consider running
/rectifyfor architectural immunity after resolving the immediate issue.External Research
workspace-writesandbox protects.git/as read-only by design (Linux landlock/bwrap). Headlesscodex execcannot use--sandbox danger-full-accesswithapproval_policy=never. This constraint is structural and unfixable from AutoSkillit's side. Source: openai.com/codex/concepts/sandboxing, openai/codex PR #19852. [SUPPORTED].git/). AutoSkillit's static capability declaration is the most robust multi-backend pattern but is consumed only mid-pipeline, not at admission with correct reachability. [SUPPORTED]Scope Boundary
Investigated:
_is_vacuous_gatelogic and its reachability blind spot; all three affected recipes (implementation, remediation, implementation-groups);_prune_skipped_stepsroute-repair mechanism;_compute_capability_feasibilityfull logic; all five admission-control enforcement surfaces (normal open_kitchen, deferred open_kitchen, get_recipe, load_recipe, dispatch_food_truck); 16-commit fix chain; installed vs. repo version parity; existing BFS reachability machinery; adversarial challenge of root cause (7 counterhypotheses tested, all refuted).Not yet explored: Whether
load_recipesoft enforcement (sets flag but doesn't early-return attools_recipe.py:240-245) is a latent exploit path; whetherpurpose_criticalannotation is still the correct long-term fix or whether reachability alone is sufficient; exact GitHubfail-label issue count attributable to this family; whether research/merge-prs recipes silently degrade on Codex (no gate step, so no DOA signal).Recommendations
Single recommendation: Add post-prune reachability check to
_is_vacuous_gate._is_vacuous_gate(_recipe_composition.py:178-213) must verify that the gate step is NOT reachable from any surviving step before declaring it vacuous. The check should use the existing_collect_all_route_targetshelper (same file, line 25) which already covers all routing fields (on_success,on_failure,on_context_limit,on_rate_limit,on_exhausted,on_resultconditions).After the
for cap_key in gate_input_keysloop completes (line 212 is inside the loop body), at the same indentation level asreturn True(line 213):post_prune_stepsfor any step (other than the gate itself) whose route targets includegate_step_nameFalse— the gate is reachable and therefore not vacuousThis leverages existing infrastructure (
_collect_all_route_targetsat line 25) and adds a single structural check to close the reachability gap. No schema changes, no new registries, no heuristic layers.Complementary actions:
test_codex_implementation_dispatch_infeasibleto assertdispatch_feasible=False(reversing PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130's flip back to [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094's correct contract)load_recipeenforcement attools_recipe.py:240-245from soft (flag-only) to hard (early-return refusal)Killed alternatives:
_is_vacuous_gateentirely — killed: re-introduces the false-positive from [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 for any future recipe with genuinely unreachable gatespurpose_criticalannotation (REQ-SCHEMA-001) — killed as immediate fix: correct long-term but scope is too large for the acute issue; reachability check is sufficient and composable with purpose_critical if added laterskip_when_falsetocreate_impl_worktree— killed:create_impl_worktreecorrectly executes at host level (unsandboxed); blocking it blocks the worktree for non-gate usesbackend_supports_git_writecheck in_check_dispatch_feasibility— killed: same rot class as prior fixes; does not generalize to next capabilityBreakage Analysis
_is_vacuous_gatedispatch_feasible=Truefor Codex+implementation:test_backend_ingredient_injection.py:554-564,test_bundled_recipes_dispatch_ready.py:268-282,test_capability_admission_e2e.py(vacuous-gate test). All must be updated to assertdispatch_feasible=Falseandinfeasible_steps=["gate_backend_write"].git log --grep="revert" -i)gate_backend_writestep is retained as defense-in-depth;skip_when_falsepruning unchanged; research/merge-prs recipes unaffected (nogate_backend_writestep). Fleet dispatch gains earlier refusal.ingredients_only=truediscovery flow is unaffected (infeasibility is on the recipe, not the ingredients).open_kitchenpath (tools_kitchen.py:731-734) is a second enforcement site;load_recipe(tools_recipe.py:240-245) needs hard enforcement upgrade; fleetdispatch_food_truck(tools_fleet_dispatch.py:340-351) already checksdispatch_feasible._is_vacuous_gate), test inversions, and one soft→hard enforcement upgrade inload_recipe.Confidence Levels
_is_vacuous_gatemissing reachability check is the root causegate_backend_writereachable viacreate_impl_worktreepurpose_criticalannotation (REQ-SCHEMA-001) never implementedload_recipesoft enforcement is a latent gapCloses #4161
Implementation Plan
Plan file:
/home/talon/projects/autoskillit-runs/remediation-20260630-104324-145470/.autoskillit/temp/rectify/rectify_vacuous_gate_reachability_2026-06-30_104500.md🤖 Generated with Claude Code via AutoSkillit
Token Usage Summary
* Step used a non-Anthropic provider; caching behavior may differ.
Token Efficiency
Model Usage Breakdown